﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Timers;
using Main.Packets;
using System.IO;
using Main;

namespace Core.Connexions
{
    public class ConnexionReseau
    {
        private static List<ConnexionReseau> listeConnexions = new List<ConnexionReseau>();

        private bool pretATransmettre;
        private byte idConnexionTransport;
        private byte idConnexionLogique;
        private EtatsConnexion etat;
        private byte adresseSource;
        private byte adresseDestination;
        private int pr;
        private int ps;
        private List<DataPacket> dataPacketsWaiting = new List<DataPacket>();
        private Timer timer;
        private bool timerElapsed;

        private ConnexionReseau(byte idConnexionTransport, byte adresseSource, byte adresseDestination)
        {
            if (listeConnexions.Count > 0)
                this.idConnexionLogique = (byte)(listeConnexions.Max(c => c.idConnexionLogique) + 1);
            else
                this.idConnexionLogique = 1;

            this.idConnexionTransport = idConnexionTransport;
            this.etat = EtatsConnexion.AttenteConnexion;
            this.adresseSource = adresseSource;
            this.adresseDestination = adresseDestination;
            this.pr = 0;
            this.ps = 7;
            this.pretATransmettre = true;
            this.timerElapsed = false;
            timer = new Timer(REGLES.DELAI_TEMPORISATEUR);
            timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
        }

        #region Propriétés

        public bool PretATransmettre { get { return pretATransmettre && dataPacketsWaiting.Count > 0; } }
        public byte IdConnexionTransport { get { return idConnexionTransport; } }
        public byte IdConnexionLogique { get { return idConnexionLogique; } }
        public EtatsConnexion Etat { get { return etat; } set { etat = value; } }
        public byte AdresseSource { get { return adresseSource; } }
        public byte AdresseDestination { get { return adresseDestination; } }
        public static List<ConnexionReseau> ListeConnexions { get { return listeConnexions.ToList(); } }
        public bool TimerElapsed { get { return timerElapsed; } }

        public int CurrentPS { get { return ps; } }
        public int NextPS
        {
            get
            {
                if (ps < 7)
                    ps++;
                else
                    ps = 0;

                return ps;
            }
        }
        public int CurrentPR { get { return pr; } }
        public int NextPR
        {
            get
            {
                if (pr < 7)
                    pr++;
                else
                    pr = 0;

                return pr;
            }
        }

        #endregion

        /// <summary>
        /// Ajoute un paquet de données à la file des paquets de données en attente d'envoie
        /// </summary>
        /// <param name="dataPacket"></param>
        public void AddDataPacketWaiting(DataPacket dataPacket)
        {
            dataPacketsWaiting.Add(dataPacket);
        }

        /// <summary>
        /// Obtient le prochain paquet en attente dans la file.
        /// </summary>
        /// <returns></returns>
        public DataPacket GetFirstDataPacketWaiting()
        {
            if (dataPacketsWaiting.Any())
                return dataPacketsWaiting[0];

            return null;
        }
        
        /// <summary>
        /// Supprime le prochain paquet en attente dans la file.
        /// </summary>
        /// <param name="dataPacket"></param>
        public void DeleteFirstDataPacketWaiting(DataPacket dataPacket)
        {
            if (dataPacketsWaiting.Any())
                dataPacketsWaiting.RemoveAt(0);
        }

        /// <summary>
        /// Méthode qui retourne une connexion réseau selon les paramètres idConnexionTransport, adresseSource et adresseDestination.
        /// </summary>
        /// <returns>Retourne la connexion existante si existe déjà ou sinon une nouvelle connexion</returns>
        public static ConnexionReseau GetConnexion(byte idConnexionTransport, byte adresseSource, byte adresseDestination)
        {
            ConnexionReseau connexion = listeConnexions.Find(c => c.adresseSource == adresseSource && c.adresseDestination == adresseDestination);
            if (connexion == null)
            {
                connexion = new ConnexionReseau(idConnexionTransport, adresseSource, adresseDestination);
                listeConnexions.Add(connexion);
            }
            return connexion;
        }

        /// <summary>
        /// Méthode qui retourne une connexion réseau selon le paramètre idConnexionTransport.
        /// </summary>
        /// <returns>Retourne la connexion existante si existe. Sinon, retourne null</returns>
        public static ConnexionReseau GetConnexionFromTransport(byte idConnexionTransport)
        {
            ConnexionReseau connexion = listeConnexions.Find(c => c.idConnexionTransport == idConnexionTransport);
            return connexion;
        }

        /// <summary>
        /// Méthode qui retourne une connexion réseau selon le paramètre idConnexionLogique.
        /// </summary>
        /// <returns>Retourne la connexion existante si existe. Sinon, retourne null</returns>
        public static ConnexionReseau GetConnexionFromReseau(byte idConnexionLogique)
        {
            ConnexionReseau connexion = listeConnexions.Find(c => c.idConnexionLogique == idConnexionLogique);
            return connexion;
        }

        /// <summary>
        /// Simule l'envoie vers la couche liaison et éventuellement du système b et simulation de la réponse du système b.
        /// </summary>
        /// <param name="packetEnvoye">Le paquet qui est envoyé vers la couche inférieure (liaison)</param>
        public Packet EnvoieVersLiaison(Packet packetEnvoye)
        {
            Packet packetRecu = null;       //Le paquet qui est reçu de la couche liaison en réponse au paquet envoyé.

            //Écriture dans le fichier L_ECRITURE (envoie vers liaison).
            using (StreamWriter sw = new StreamWriter(REGLES.L_ECRITURE, true))
            {
                packetEnvoye.Send(sw);
            }

            //Début de la simulation de la réponse du système b selon l'envoie.
            switch (packetEnvoye.PacketType.Type)
            {
                case PacketTypes.ConnectionRequestPacket:
                    ConnectionPacket connectionPacketEnvoye = (ConnectionPacket)packetEnvoye;
                    this.etat = EtatsConnexion.AttenteConfirmation;
                    if ((connectionPacketEnvoye.AdresseSource % REGLES.MULTIPLE_POUR_REFUS_CONNEXION_DISTANT) == 0)
                    {
                        packetRecu = FreeConnectionPacket.CreateFreeConnectionIndicationPacket(connectionPacketEnvoye.NumeroConnexion, connectionPacketEnvoye.AdresseDestination, connectionPacketEnvoye.AdresseSource, Raisons.DistantRefuse);
                    }
                    else if (!((connectionPacketEnvoye.AdresseSource % REGLES.MULTIPLE_POUR_ABSENCE_REPONSE_CONNEXION) == 0))
                    {
                        packetRecu = ConnectionPacket.CreateConnectionConfirmationPacket(connectionPacketEnvoye.NumeroConnexion, connectionPacketEnvoye.AdresseDestination, connectionPacketEnvoye.AdresseSource);
                    }
                    else
                    {
                        timer.Start();
                    }
                    break;

                case PacketTypes.DataPacket:
                    DataPacket dataPacketEnvoye = (DataPacket)packetEnvoye;

                    if (!((this.adresseSource % REGLES.MULTIPLE_POUR_ABSENCE_ACQUITTEMENT) == 0))
                    {
                        int packetPs = Convert.ToInt32(dataPacketEnvoye.PacketType.Bits4To2, 2);

                        if (AquittementNegatif(packetPs))
                        {
                            packetRecu = AcknowledgementPacket.CreateNegativeAcknowledgementPacket(idConnexionLogique, packetPs);

                            if (dataPacketEnvoye.NombreRetransmission < REGLES.FENETRE_ENVOIE_PACKET)
                                pretATransmettre = true;
                            else
                                pretATransmettre = false;
                            dataPacketEnvoye.IncrementerNombreRetransmissions();
                        }
                        else
                        {
                            dataPacketsWaiting.RemoveAt(0);
                            pretATransmettre = true;
                            EnvoieSystemeBVersLiaison(AcknowledgementPacket.CreatePositiveAcknowledgementPacket(idConnexionLogique, packetPs + 1));
                        }
                    }
                    else
                    {
                        pretATransmettre = false;
                        timerElapsed = false;
                        timer.Start();
                    }
                    break;
                case PacketTypes.FreeConnectionPacket:
                    FreeConnectionPacket freeConnectionPacket = (FreeConnectionPacket)packetEnvoye;
                    packetRecu = FreeConnectionPacket.CreateFreeConnectionIndicationPacket(freeConnectionPacket.NumeroConnexion, freeConnectionPacket.AdresseDestination, freeConnectionPacket.AdresseSource, Raisons.DistantRefuse);
                    break;
            }

            EnvoieSystemeBVersLiaison(packetRecu);
            return packetRecu;

        }

        /// <summary>
        /// Écrit dans le fichier L_LECTURE le paquet que le système b aurait retourné.
        /// </summary>
        /// <param name="p"></param>
        private void EnvoieSystemeBVersLiaison(Packet p)
        {
            if (p != null)
            {
                using (StreamWriter sw = new StreamWriter(REGLES.L_LECTURE, true))
                {
                    ConnexionReseau connReseau = ConnexionReseau.GetConnexionFromReseau(p.NumeroConnexion);

                    if (!((connReseau.AdresseSource % REGLES.MULTIPLE_POUR_ABSENCE_REPONSE_CONNEXION) == 0))
                        p.Send(sw);
                }
            }
        }

        /// <summary>
        /// Détermine si l'aquittement est négatif
        /// </summary>
        private bool AquittementNegatif(int ps)
        {
            Random rand = new Random();
            int randNbr = rand.Next(0, 7);
            return (ps == randNbr);
        }

        /// <summary>
        /// Détruit la connexion. (Enlève la connexion de la liste de connexions static).
        /// </summary>
        public void Destroy()
        {
            timer.Dispose();
            listeConnexions.Remove(this);
        }

        /// <summary>
        /// Se produit lorsque le temps du temporisateur est écoulé
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            if (this.etat == EtatsConnexion.ConnexionEtablie)
            {
                if (dataPacketsWaiting[0].NombreRetransmission < REGLES.FENETRE_ENVOIE_PACKET)
                    pretATransmettre = true;
                dataPacketsWaiting[0].IncrementerNombreRetransmissions();
            }
            timerElapsed = true;
            timer.Stop();
        }
    }
}
